feat(devx): gate the os package publish example set against its documented block - #15721
Conversation
…umented block Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…terals `check:cli-command-ids` reads any quoted run opening with a real oclif bin name as a command-id literal, and reds when it resolves to nothing. Two fixtures in the new parity gate's self-test were deliberately-unresolvable phrases. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…hake The handshake guards a `return` that skips the verdict block, not the verdict's direction; leaving the flag false on a red printed "never finished" over a run that finished and failed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
|
ACCEPT — PM seat Reviewed against the three-dot diff at What the diff does: new Measured by this seat on the PR head (plain The derived family (53 commands, 50 exit 0, 3 NOT MEASURED by name) is the dev's reading; CI's Fences held: three files; Generated by Claude Code |
Fixes #15393
os package publishships its example set twice — the oclifexamplesarray inpackages/cli/src/commands/package/publish.ts, printed verbatim underEXAMPLES, and afenced block under
#### os package publishincontent/docs/deployment/cli.mdx. Twohand-maintained copies of one set, and nothing compared them. They came apart: the CLI
line carried
# local dev (apps/cloud), pointing at a directory deleted from this repo,while the docs line for the same example was correct the whole time the shipped
--helpoutput was wrong. PR #15390 made them agree again; this is the mechanism half.
No change to
publish.tsorcli.mdx— the two sides already agree, and this PRmeasures that rather than asserting it.
What ships
scripts/check-cli-examples-parity.mjs— the gate, with--listand--self-test.package.json— one row,check:cli-examples-parity,--self-test && gate, next tocheck:cli-command-ids..github/workflows/lint.yml— one step in theLint & Repo Gatesjob, immediatelyafter the
CLI command-id literals resolvestep. No build, no extra install: the gatereads two source files.
⛔ Not an extension of
check-cli-command-idsThat gate considered this same docs page and refused, on the record at
scripts/check-cli-command-ids.mjs:99-106: extending it over prose would mean deciding,with no delimiter to lean on, which of 811
os ...mentions incontent/docsis acommand and which is a sentence. That refusal is correct, it stands untouched, and this
PR does not reopen it. The new gate's header says so in its own words.
The difference is the population. This one is a table, not a corpus:
packages/cli/src/commands/package/publish.tsexamplescontent/docs/deployment/cli.mdx#### os package publishOne fenced block under one exact heading, against one named array in one named file. No
prose is scanned and no line enters the comparison that a row did not point at. A second
pair is one more row.
The equality rule
The two voices legitimately differ — help text is terse, docs prose annotates, the CLI
writes
$prompts, the docs block aligns its comments into a column, and the ordersdiffer today (
--installis third in the CLI and fourth in the docs). So what iscompared is the set of invocations, each normalised by:
$prompt;#comment, only where a shell would take it — at line start orafter whitespace, and outside quotes, so the
#in--note "a # b"stays part of theinvocation;
Order is ignored. Commentary is ignored. The invocations may not differ.
The one definitional call, measured before it was made
The docs block carries six lines; the
examplesarray carries five. The extradocs line is
os compile— the step a reader runs before publishing, in a blockwritten to be pasted whole. It is not a sixth way to invoke
os package publish, and anoclif
examplesentry is by construction an example of the command it is declared on,so the array could not carry it.
So the row's command phrase — derived from the command file's path plus the owning
package's declared
oclif.bin, never typed a second time — selects which fence lines arecompared. A fence line that does not invoke that phrase is a context line: counted,
named in the verdict, not compared. Comparing every fence line instead would assert
something false about docs blocks (that a teaching transcript may contain nothing but
invocations of one command) and would red today over a line nobody thinks is wrong.
The asymmetry runs the other way on the CLI side, deliberately: an
examplesentry thatdoes not invoke the row's command is a finding, never a silently skipped line.
line must match, that makes today's tree red and the repair lands in
cli.mdx, whichthis lane treats as read-only — say so and it comes back as a decision, not a patch.
Refusals, never quiet passes
Exit 1 naming the row for: an empty table; a command file, docs page or package manifest
that cannot be read; an
examplessymbol that is absent or is not a plain array ofstring literals (a computed array or an interpolating template is refused, not guessed
at); a heading that is absent or ambiguous; a heading with no fenced block before the
next heading; and zero invocations on either side — the anti-vacuity case, where two
empty sets would otherwise be equal and print a confident green.
The CommonMark fence scanner is imported from
scripts/docs-audit/check-docs-transcript-drift.mjsrather than re-derived, which alsorecords the coupling where
scripts/pm/dispatch-gates.mjscan see it.Measured on this tree
node scripts/check-cli-examples-parity.mjs— exit 0:--listprints both sides; the five normalised invocations are identical strings onboth, and
os compileis the single context line.Self-test
node scripts/check-cli-examples-parity.mjs --self-test— exit 0, 49 cases, threebatteries under a declared roster and floor (the shape the sibling gates use):
examplesreader and its refusals, theheading/fence locator — 22 cases;
judged in the same run as the first (one green, one red, each finding attributed to
its own row) — 16 cases;
codes — equal sets in a different order with different comments exit 0; an invocation
missing on one side exits 1 naming it; a docs block with zero invocations of the
command exits 1; a moved heading exits 1; an absent
examplessymbol exits 1 — 8cases.
Ablation (trap-guarded, absolute paths, mutation proven on disk both directions)
Removed the trailing-comment normalisation (
text = stripTrailingComment(text);, oneoccurrence, replaced by a marker):
Dispatch derivation, before and after
node scripts/pm/dispatch-gates.mjs --commands PATH --repo objectstack-ai/objectstack,both sides of the pair (exit 0 each):
packages/cli/src/commands/package/publish.tspnpm check:cli-examples-paritycontent/docs/deployment/cli.mdxpnpm check:cli-examples-parityThe table rows are the path literals that place it, which is why both sides name it.
The derived family on the final head
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstackderives 53 commands for this diff; all were run. Green includes
check:cli-command-ids,check-self-test-wired(+--self-test),check-self-test-workflow-commands(+--self-test),check:ci-filter-parity,check:watch-hint-literal,check:declared-population-live,check:pm-dispatch-gates,check:nul-bytes,check-aggregator-roster,check-declaration-mirrors,check-step-collectors,check:required-contexts,check:parse-guard,check:entry-guardandcheck:turbo-task-graph.check:cli-command-idsdid red on the first draft — two self-test fixtures werequoted phrases opening with a real bin name and resolving to nothing. Reworded (a
neutral binary for the word-boundary case, a plain heading for the absent-heading case);
green since.
⊘ NOT MEASURED
check:dual-build-cjs-loadsandcheck:type-check-debt— both exit 3,PREREQUISITE NOT MET: they read built output and this tree has nodist/. Not apass and not a finding. Neither is implicated by this diff (both are derived from
package.json).⊘ NOT MEASURED(
check-shard-attestation,check-test-completeness, the--union-intolimb ofcheck-cross-package-test-inputs) — their argv carries values that exist only on arunner.
pnpm lintrepo-wide: narrowed, and the narrowing is measured, not assumed.(1) The population comes from eslint's own flat config, not from a guess: run over the
three changed paths, eslint reports
File ignored because no matching configuration was suppliedforpackage.jsonand.github/workflows/lint.yml— they are outsideits population entirely. (2) The count comes from
--format json: 3 files passed in,1 linted, 0 errors, 0 warnings on
scripts/check-cli-examples-parity.mjs.(3) Invariance for untouched files: the flat config enables no type-aware linting
(
eslint.config.mjs:328states it, and noparserOptions.projectexists in thefile), so this diff cannot move the verdict on any file it does not contain.
skip-changeset. Nothing here is published from any package — a gatescript, a root
package.jsonscript row and a CI step.Scope
The card's disposition was gate it and nothing else.
publish.tsandcli.mdxareuntouched,
scripts/check-cli-command-ids.mjsis untouched, and no bare token sweep ofobjectosorcloudwas performed anywhere — that trap is real and is recorded on#14806.
🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code